package net.pantolomin.nayo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.RejectedExecutionException;

import net.pantolomin.nayo.api.Connection;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The implementation for a connection
 */
final class NioConnection implements Connection {
	/** The logger */
	private final Logger logger = LoggerFactory.getLogger(NioConnection.class);

	/** The associated connector */
	private final NioConnector<? extends AbstractSelectableChannel> connector;

	/** The socket channel */
	private final SocketChannel socketChannel;

	/** The selection key */
	private final SelectionKey selectionKey;

	/** The writer */
	private final DataWriter writer;

	/** The reader */
	private final DataReader reader;

	/** The task handling the ping */
	private TimerTask idleDetectionTask;

	/** The time for the last data that was received from the remote host */
	private long lastDataReceived;

	/** Indicates if the connection is closed */
	private volatile boolean closed;

	/**
	 * Create the context
	 * 
	 * @param connector
	 *          the associated connector
	 * @param socketChannel
	 *          the socket channel
	 * @param selectionKey
	 *          the selection key
	 */
	NioConnection(NioConnector<? extends AbstractSelectableChannel> connector, SocketChannel socketChannel, SelectionKey selectionKey) {
		this.connector = connector;
		this.socketChannel = socketChannel;
		this.selectionKey = selectionKey;
		this.writer = new DataWriter(this);
		this.reader = new DataReader(this);
	}

	public InetSocketAddress getLocalAddress() throws IOException {
		return (InetSocketAddress) this.socketChannel.getLocalAddress();
	}

	public InetSocketAddress getRemoteAddress() throws IOException {
		return (InetSocketAddress) this.socketChannel.getRemoteAddress();
	}

	/**
	 * @return the associated connector
	 */
	NioConnector<? extends AbstractSelectableChannel> getConnector() {
		return this.connector;
	}

	/**
	 * @return the provider
	 */
	NioProvider getProvider() {
		return this.connector.getProvider();
	}

	/**
	 * @return the socket channel
	 */
	SocketChannel getSocketChannel() {
		return this.socketChannel;
	}

	/**
	 * @return the selection key
	 */
	SelectionKey getSelectionKey() {
		return this.selectionKey;
	}

	/**
	 * @return the writer
	 */
	DataWriter getWriter() {
		return this.writer;
	}

	/**
	 * @return the reader
	 */
	DataReader getReader() {
		return this.reader;
	}

	/**
	 * @param timer
	 *          the timer
	 * @param maxIdleTime
	 *          the maximum allowed time without receiving data
	 */
	void scheduleCheckIdle(final Timer timer, final long maxIdleTime) {
		this.lastDataReceived = System.currentTimeMillis();
		this.idleDetectionTask = getNextCheckTask(timer, maxIdleTime);
		timer.schedule(this.idleDetectionTask, maxIdleTime + 100L);
	}

	/**
	 * @param timer
	 *          the timer
	 * @param maxIdleTime
	 *          the max time to be idle
	 * @return the task
	 */
	private TimerTask getNextCheckTask(final Timer timer, final long maxIdleTime) {
		return new TimerTask() {
			@Override
			@SuppressWarnings("synthetic-access")
			public void run() {
				long nextCheck = maxIdleTime - System.currentTimeMillis() + lastDataReceived;
				if (nextCheck <= 0L) {
					getProvider().getSelectionLoop().closeConnectionByRemote(getSocketChannel(), false);
					return;
				}
				idleDetectionTask = getNextCheckTask(timer, maxIdleTime);
				timer.schedule(idleDetectionTask, nextCheck + 100L);
			}
		};
	}

	/**
	 * Mark this connection as closed
	 */
	void doClose() {
		this.closed = true;
		this.selectionKey.cancel();
		this.idleDetectionTask.cancel();
		try {
			this.socketChannel.close();
		} catch (IOException e) {
			this.logger.warn("Failed to close the connection: {}", this.socketChannel, e);
		}
	}

	/**
	 * Notifies that some data was read
	 * 
	 * @param data
	 *          the read data
	 */
	void dataRead(byte[] data) {
		this.lastDataReceived = System.currentTimeMillis();
		try {
			this.connector.getEventsListener().dataRead(this, data);
		} catch (Exception e) {
			this.logger.warn("Failed to handle an incoming message", e);
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(512);
		sb.append(getClass().getSimpleName());
		InetSocketAddress addr;
		try {
			addr = (InetSocketAddress) this.socketChannel.getRemoteAddress();
			sb.append('[').append(addr.getAddress() != null ? addr.getAddress().getHostAddress() : "N/A");
			sb.append(':').append(addr.getPort()).append(']');
		} catch (IOException e) {
			this.logger.error("Failed to get the remote address for the channel", e);
		}
		return sb.toString();
	}

	//
	//
	//
	//
	//
	//
	//
	//
	//
	//
	// ************************************************************************
	// ************************************************************************
	// Public API
	// ************************************************************************
	// ************************************************************************

	public boolean isConnected() {
		return !this.closed;
	}

	public void send(ByteBuffer data) throws IOException {
		if (this.closed) {
			throw new IOException("Connection closed");
		}
		this.writer.addDataToSend(data);
		try {
			this.writer.askForWrite(this.connector.getProvider().getWriteExecutor());
		} catch (RejectedExecutionException e) {
			throw new IOException("No available execution unit to send the data", e);
		}
	}

	public void close() {
		this.connector.getProvider().getSelectionLoop().closeChannel(this.socketChannel);
	}
}